package io.cici.cc.mybatis.lite.builder.xml;

import io.cici.cc.mybatis.lite.builder.BuilderException;
import io.cici.cc.mybatis.lite.parse.FixedVariableParser;
import io.cici.cc.mybatis.lite.parse.Node;
import io.cici.cc.mybatis.lite.session.Configuration;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.NodeList;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;

public class XmlIncludeTransformer {

    private final Configuration configuration;

    public XmlIncludeTransformer(Configuration configuration) {
        this.configuration = configuration;
    }

    public void applyIncludes(org.w3c.dom.Node node) {
        Properties variablesContext = new Properties();
        Properties configurationVariables = configuration.getVariables();
        Optional.ofNullable(configurationVariables).ifPresent(variablesContext::putAll);
        applyIncludes(node, variablesContext, false);
    }

    /**
     * Recursively apply includes through all SQL fragments.
     *
     * @param node             Include node in DOM tree
     * @param variablesContext Current context for static variables with values
     */
    private void applyIncludes(org.w3c.dom.Node node, final Properties variablesContext, boolean included) {
        if ("include".equals(node.getNodeName())) {
            org.w3c.dom.Node toInclude = findSqlFragment(getStringAttribute(node, "refid"), variablesContext);
            Properties toIncludeContext = getVariablesContext(node, variablesContext);
            applyIncludes(toInclude, toIncludeContext, true);
            if (toInclude.getOwnerDocument() != node.getOwnerDocument()) {
                toInclude = node.getOwnerDocument().importNode(toInclude, true);
            }
            node.getParentNode().replaceChild(toInclude, node);
            while (toInclude.hasChildNodes()) {
                toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
            }
            toInclude.getParentNode().removeChild(toInclude);
        } else if (node.getNodeType() == org.w3c.dom.Node.ELEMENT_NODE) {
            if (included && !variablesContext.isEmpty()) {
                // replace variables in attribute values
                NamedNodeMap attributes = node.getAttributes();
                for (int i = 0; i < attributes.getLength(); i++) {
                    org.w3c.dom.Node attr = attributes.item(i);
                    attr.setNodeValue(FixedVariableParser.parse(attr.getNodeValue(), variablesContext));
                }
            }
            NodeList children = node.getChildNodes();
            for (int i = 0; i < children.getLength(); i++) {
                applyIncludes(children.item(i), variablesContext, included);
            }
        } else if (included
                && (node.getNodeType() == org.w3c.dom.Node.TEXT_NODE || node.getNodeType() == org.w3c.dom.Node.CDATA_SECTION_NODE)
                && !variablesContext.isEmpty()) {
            // replace variables in text node
            node.setNodeValue(FixedVariableParser.parse(node.getNodeValue(), variablesContext));
        }
    }

    private org.w3c.dom.Node findSqlFragment(String refid, Properties variables) {
        refid = FixedVariableParser.parse(refid, variables);
        try {
            Node nodeToInclude = configuration.getSqlIdToNodeMap().get(refid);
            return nodeToInclude.getNode().cloneNode(true);
        } catch (IllegalArgumentException e) {
            throw new IllegalArgumentException("Could not find SQL statement to include with refid '" + refid + "'", e);
        }
    }

    private String getStringAttribute(org.w3c.dom.Node node, String name) {
        return node.getAttributes().getNamedItem(name).getNodeValue();
    }

    /**
     * Read placeholders and their values from include node definition.
     *
     * @param node                      Include node instance
     * @param inheritedVariablesContext Current context used for replace variables
     *                                  in new variables values
     * @return variables context from include instance (no inherited values)
     */
    private Properties getVariablesContext(org.w3c.dom.Node node, Properties inheritedVariablesContext) {
        Map<String, String> declaredProperties = null;
        NodeList children = node.getChildNodes();
        for (int i = 0; i < children.getLength(); i++) {
            org.w3c.dom.Node n = children.item(i);
            if (n.getNodeType() == org.w3c.dom.Node.ELEMENT_NODE) {
                String name = getStringAttribute(n, "name");
                // Replace variables inside
                String value = FixedVariableParser.parse(getStringAttribute(n, "value"), inheritedVariablesContext);
                if (declaredProperties == null) {
                    declaredProperties = new HashMap<>();
                }
                if (declaredProperties.put(name, value) != null) {
                    throw new BuilderException("Variable " + name + " defined twice in the same include definition");
                }
            }
        }
        if (declaredProperties == null) {
            return inheritedVariablesContext;
        }
        Properties newProperties = new Properties();
        newProperties.putAll(inheritedVariablesContext);
        newProperties.putAll(declaredProperties);
        return newProperties;
    }
}
